/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.polarrose.json;

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.CharEncoding;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.ByteArrayOutputStream;
import java.net.URL;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.util.*;

public class JsonSerializer
{
    private static final Set<Class<?>> simpleTypes = new HashSet<Class<?>>();

    static {
        simpleTypes.add(Boolean.class);
        simpleTypes.add(Byte.class);
        simpleTypes.add(Character.class);
        simpleTypes.add(Short.class);
        simpleTypes.add(Integer.class);
        simpleTypes.add(Long.class);
        simpleTypes.add(Float.class);
        simpleTypes.add(Double.class);
    }

    private static final Set<Class<?>> simpleStringTypes = new HashSet<Class<?>>();

    static {
        simpleStringTypes.add(String.class);
        simpleStringTypes.add(UUID.class);
        simpleStringTypes.add(URL.class);
    }

    private static final Package javaPackage = Package.getPackage("java");

    //

    public String serialize(Object object) throws IOException
    {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        serializeToOutputStream(object, baos);
        return baos.toString(CharEncoding.UTF_8);
    }

    public void serializeToOutputStream(Object object, OutputStream outputStream) throws IOException
    {
        PrintStream printStream = new PrintStream(outputStream);
        serializeObject(object, printStream);
    }

    //

    private void serializeObject(Object object, PrintStream printStream)
    {
        if (object == null) {
            serializeNullObject(printStream);
        } else {
            Class<?> clazz = object.getClass();

            if (simpleTypes.contains(clazz)) {
                serializeSimpleType(object, printStream);
            }

            else if (simpleStringTypes.contains(clazz)) {
                serializeSimpleStringType(object, printStream);
            }

            else if (clazz.isPrimitive()) {
                serializePrimitiveType(object, printStream);
            }

            else if (clazz.isArray()) {
                serializeArray(object, printStream);
            }

            else if (clazz.isEnum()) {
                serializeEnum((Enum) object, printStream);
            }

            else if (object instanceof List) {
                serializeList((List) object, printStream);
            }

            else if (object instanceof Map) {
                serializeMap((Map) object, printStream);
            }

            else if (!clazz.getPackage().equals(javaPackage)) {
                serializeJavaBean(object, printStream);
            }
        }
    }

    private void serializeJavaBean(Object object, PrintStream printStream)
    {
        PropertyDescriptor[] propertyDescriptors = getPropertyDescriptors(object.getClass());
        if (propertyDescriptors != null && propertyDescriptors.length != 0) {
            printStream.print("{");
            boolean first = true;
            for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
                if (propertyDescriptor.getName().compareTo("class") == 0) {
                    continue;
                }
                try {
                    if (first) {
                        first = false;
                    } else {
                        printStream.print(",");
                    }
                    Object value = propertyDescriptor.getReadMethod().invoke(object);
                    printStream.print("\"" + propertyDescriptor.getName() + "\":");
                    serializeObject(value, printStream);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException("");
                } catch (InvocationTargetException e) {
                    throw new RuntimeException("");
                }
            }
            printStream.print("}");
        }
    }
    
    private void serializeNullObject(PrintStream printStream)
    {
        printStream.print("null");
    }

    private void serializeSimpleStringType(Object object, PrintStream printStream)
    {
        printStream.print('"');
        printStream.print(StringEscapeUtils.escapeJava(object.toString()));
        printStream.print('"');
    }

    private void serializeSimpleType(Object object, PrintStream printStream)
    {
        printStream.print(object);
    }

    private void serializeEnum(Enum e, PrintStream printStream)
    {
        printStream.print(e); // TODO Make this configurable? Ord value or name?
    }

    private void serializePrimitiveType(Object o, PrintStream printStream)
    {
        printStream.print(o.toString());
    }

    private void serializeList(List list, PrintStream printStream)
    {
        printStream.print("[");

        for (int i = 0; i < list.size(); i++) {
            if (i != 0) {
                printStream.print(",");
            }
            serializeObject(list.get(i), printStream);
        }

        printStream.print("]");
    }

    private void serializeMap(Map map, PrintStream printStream)
    {
        Boolean first = true;

        printStream.print("{");

        for (Object o : map.keySet()) {
            if (!first) {
                printStream.print(",");
            }
            printStream.print('"');
            printStream.print(o);
            printStream.print("\":");
            serializeObject(map.get(o), printStream);
            first = false;
        }

        printStream.print("}");
    }

    private void serializeArray(Object array, PrintStream printStream)
    {
        printStream.print("[");

        for (int i = 0; i < Array.getLength(array); i++) {
            if (i != 0) {
                printStream.print(",");
            }
            Object object = Array.get(array, i);
            serializeObject(object, printStream);
        }

        printStream.print("]");
    }

    //

    public static PropertyDescriptor[] getPropertyDescriptors(Class clazz)
    {
        try {
            BeanInfo info = Introspector.getBeanInfo(clazz);
            return info.getPropertyDescriptors();
        } catch (IntrospectionException ie) {
            throw new RuntimeException("Could not examine class '" + clazz.getName() +
                "' using Introspector.getBeanInfo() to determine property information.", ie);
        }
    }
}
